package file.manager;

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.TreeMap;

import file.BadFileTypeException;
import file.TransferFile;
import file.TransferFileObserver;
import file.network.FileReceiver;
import file.network.FileSender;

import user.User;
import utils.Configuration;

import network.AbstractNetworkInterface;
import network.SendSignalException;
import network.config.NetworkConfiguration;
import network.config.NetworkConfiguration.PortType;


public class TransferFileManager extends Thread {
	
	private static final int TCP_SOCKET_TIMEOUT = 50 ;
	
	private TreeMap <Integer, TransferFile> waitingSendFiles = null ;
	private LinkedList <TransferFile> waitingRecvFiles = null ;
	private TreeMap <String, TransferFile> waitingAcceptFiles = null ;
	private ArrayList <TransferFileObserver> tfos = null ;
	
	private boolean running ;
	private ServerSocket server ;
	private AbstractNetworkInterface networkInterface ;
	private NetworkConfiguration networkConfig ;

	/**
	 * @param messageController 
	 * @throws FileManagerStartException 
	 * @throws IOException if an error occurs creating the server
	 */
	public TransferFileManager (AbstractNetworkInterface networkInterface, NetworkConfiguration netConf) throws FileManagerStartException {
		this.networkInterface = networkInterface ;
		this.networkConfig = netConf ;
		this.waitingSendFiles = new TreeMap <Integer, TransferFile> () ;
		this.waitingRecvFiles = new LinkedList <TransferFile> () ;
		this.waitingAcceptFiles = new TreeMap <String, TransferFile> () ;
		this.tfos = new ArrayList <TransferFileObserver> () ;
		try {
			Configuration.file_debug.println("Starting file receiver on port : " + this.networkConfig.getPort(PortType.TCP_ACCEPT)) ;
			this.server = new ServerSocket(this.networkConfig.getPort(PortType.TCP_ACCEPT)) ;
			this.server.setSoTimeout(TCP_SOCKET_TIMEOUT) ;
		} 
		catch (IOException e) {
			throw new FileManagerStartException(e) ;
		}
	}
	
	public TransferFileObserver addTransferFileObserver(TransferFileObserver tfo) {
		this.tfos.add(tfo) ;
		return tfo;
	}

	public TransferFileObserver removeTransferFileObserver(TransferFileObserver tfo) {
		this.tfos.remove(tfo) ;
		return tfo;
	}
	
	/**
	 * Add a new file to the send queue
	 * 
	 * @param tf The file to add
	 * @return The ID of the file
	 * @throws BadFileTypeException if the file does not exist or is not readable
	 */
	public TransferFile addFileToSend (File f, User to) throws BadFileTypeException{
		if (f == null || !f.exists() || !f.canRead()) {
			throw new BadFileTypeException() ;
		}
		TransferFile tf = new TransferFile(f, nextID(), to) ;
		this.waitingSendFiles.put(tf.getID(), tf) ;
		Configuration.file_debug.println("Add file to send : " + tf.getID()) ;
		return tf;
	}
	
	/**
	 * Remove a file from the send queue without sending it
	 * 
	 * @param fileID The ID of the file
	 * @return 
	 */
	public TransferFile removeFileToSend (int fileID) {
		if (this.waitingSendFiles.containsKey(fileID)) {
			return this.waitingSendFiles.remove(fileID) ;
		}
		return null ;
	}
	
	/**
	 * Add a file to the accept queue. If the FileSendManager is started
	 * 
	 * @param wf The waiting file to receive, with the remote file name
	 */
	public void addFileToAccept (TransferFile tf) {
		this.waitingAcceptFiles.put(tf.getIdentifier(), tf) ;
	}
	
	/**
	 * Get a file waiting in the accept queue
	 * 
	 * @param identifier A unique identifier
	 * @return The TransferFile, null if not found
	 */
	public TransferFile getWaitingToAcceptFile (String identifier) {
		return this.waitingAcceptFiles.get(identifier) ;
	}
	
	/**
	 * Get a file sending in the accept queue
	 * 
	 * @param identifier A unique identifier
	 * @return The TransferFile, null if not found
	 */
	public TransferFile getWaitingToSendFile (String identifier) {
		return this.waitingSendFiles.get(identifier) ;
	}
	
	/**
	 * Send the file with the specified ID according to information stored into
	 * TransferFile.
	 * 
	 * @param fileID The ID of the file to send
	 * @return The corresponding TransferFile, or null if no File was found.
	 */
	public TransferFile sendFile (int fileID) {
		if (this.waitingSendFiles.containsKey(fileID)) {
			TransferFile tf = this.waitingSendFiles.remove(fileID) ;
			(new FileSender(tf, getObserversArray(), this.networkConfig.getPort(PortType.TCP_CONNECT))).start () ;
			return tf ;
		}
		Configuration.file_debug.println("No file found for ID : " + fileID) ;
		return null ;
	}
	
	/**
	 * Move a file from the waiting to accept queue to the waiting to receive
	 * queue. If the TransferFileManager is started, it must be notify to
	 * receive the file.
	 * 
	 * @param tf The waiting file to receive, with the new file name
	 * @return true if the file has been moved, false if the file was not found
	 */
	public boolean acceptFile (TransferFile tf) {
		if (this.waitingAcceptFiles.containsKey(tf.getIdentifier())) {
			this.waitingAcceptFiles.remove(tf.getIdentifier()) ;
			this.waitingRecvFiles.add(tf) ;
			return true ;
		}
		return false ;
	}

	/**
	 * Remove a file from the waiting accept queue
	 * 
	 * @param tf The file to remove
	 * @return true if the file was removed
	 */
	public boolean refuseFile(TransferFile tf) {
		if (this.waitingAcceptFiles.containsKey(tf.getIdentifier())) {
			this.waitingAcceptFiles.remove(tf.getIdentifier()) ;
			return true ;
		}
		return false ;
	}
	
	/**
	 * @return An array corresponding to the ArrayList of TransferFileObserver
	 */
	private TransferFileObserver[] getObserversArray () {
		TransferFileObserver[] tfos = new TransferFileObserver[this.tfos.size()] ;
		return this.tfos.toArray(tfos) ;
	}
	
	/**
	 * 
	 */
	public void finish () {
		this.running = false ;
	}
	
	public static final int MAX_TRY_FILE = 30 ;
	
	/* (non-Javadoc)
	 * @see java.lang.Thread#run()
	 */
	public void run () {
		this.running = true ;
		String currentIdentifier = "" ;
		int count = 0 ;
		while (this.running) {
			if (!this.waitingRecvFiles.isEmpty()) {
				TransferFile tf = this.waitingRecvFiles.getFirst() ;
				if (!tf.getIdentifier().equals(currentIdentifier)) {
					currentIdentifier = tf.getIdentifier() ;
					count = 0 ;
				}
				else {
					count ++ ;
				}
				Configuration.file_debug.println("File : " + tf.getFileName() + " - " + count) ;
				try {
					this.networkInterface.sendAcceptFile(tf.getSender().getAddress(), tf.getID(), true, true) ;
					Socket sender = this.server.accept() ;
					if (sender.getInetAddress().equals(tf.getSender().getAddress())) {
						(new FileReceiver(this.waitingRecvFiles.removeFirst(), getObserversArray(), sender)).start();
					}
					else {
						this.waitingRecvFiles.removeFirst() ;
					}
				} 
				catch (SocketTimeoutException e) {
					Configuration.file_debug.println("File : " + tf.getFileName() + " - time out.") ;
					if (count > MAX_TRY_FILE) {
						this.waitingRecvFiles.removeFirst() ;
					}
				} 
				catch (IOException e) {
					e.printStackTrace();
				} 
				catch (SendSignalException e) {
					e.printStackTrace();
				}
			}
			else {
				synchronized (this) {
					try {
						wait() ;
					} 
					catch (InterruptedException e) {
					}
				}
			}
		}
		try {
			this.server.close() ;
		} 
		catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Generate and return a new unique ID
	 * 
	 * @return The next available ID
	 */
	private static int nextID () {
		return (int)System.currentTimeMillis() ;
	}

}
